package com.fairysunny.reactive;

import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.*;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;

public final class ReactiveContext {
    private interface ReactiveObject {}

    private static boolean differ(Object obj1, Object obj2) {
        if (obj1 == null || obj2 == null || obj1 instanceof ReactiveObject || obj2 instanceof ReactiveObject) {
            return obj1 != obj2;
        }
        return !obj1.equals(obj2);
    }

    private final class ReactiveRef<T> implements IRef<T>, ReactiveObject {
        T item;

        final Set<Effect<?>> deps = new HashSet<>();

        ReactiveRef(T item) {
            this.item = item;
        }

        @Override
        public T get() {
            track(deps);
            return item;
        }

        @Override
        public void set(T item) {
            if (differ(this.item, item)) {
                this.item = item;
                trigger(deps);
            }
        }
    }

    private final class ReactiveList<T> extends AbstractList<T> implements ReactiveObject {
        final List<T> list;

        final List<Set<Effect<?>>> depsMap;

        Set<Effect<?>> lengthDeps = new HashSet<>();

        ReactiveList(List<T> list) {
            this.list = list;
            this.depsMap = new ArrayList<>(Collections.nCopies(list.size(), null));
        }

        void trackIndex(int index) {
            if (running != null) {
                Set<Effect<?>> deps = depsMap.get(index);
                if (deps == null) {
                    deps = new HashSet<>();
                    depsMap.set(index, deps);
                }
                track(deps);
            }
        }

        void triggerIndex(int index) {
            Set<Effect<?>> deps = depsMap.get(index);
            if (deps != null) {
                trigger(deps);
            }
        }

        @Override
        public T get(int index) {
            T element = list.get(index);
            trackIndex(index);
            return element;
        }

        @Override
        public T set(int index, T element) {
            T oldElement = list.set(index, element);
            if (differ(oldElement, element)) {
                triggerIndex(index);
            }
            return oldElement;
        }

        @Override
        public void add(int index, T element) {
            list.add(index, element);
            depsMap.add(index, null);
            for (int i = index; i < list.size() - 1; i++) {
                triggerIndex(i);
            }
            trigger(lengthDeps);
        }

        @Override
        public T remove(int index) {
            T element = list.remove(index);
            depsMap.remove(list.size());
            for (int i = index; i < list.size(); i++) {
                triggerIndex(i);
            }
            trigger(lengthDeps);
            return element;
        }

        @Override
        public int size() {
            int length = list.size();
            track(lengthDeps);
            return length;
        }
    }

    private final class ReactiveMap<K, V> extends AbstractMap<K, V> implements ReactiveObject {
        final Map<K, V> map;

        final Map<K, Set<Effect<?>>> depsMap = new HashMap<>();

        final Set<Effect<?>> lengthDeps = new HashSet<>();

        ReactiveMap(Map<K, V> map) {
            this.map = map;
        }

        void trackKey(K key) {
            if (running != null) {
                Set<Effect<?>> deps = depsMap.computeIfAbsent(key, k -> new HashSet<>());
                track(deps);
            }
        }

        void triggerKey(K key) {
            Set<Effect<?>> deps = depsMap.get(key);
            if (deps != null) {
                trigger(deps);
            }
        }

        @Override
        public Set<Entry<K, V>> entrySet() {
            Set<Entry<K, V>> mapEntrySet = map.entrySet();
            return new AbstractSet<>() {
                @Override
                public Iterator<Entry<K, V>> iterator() {
                    Iterator<Entry<K, V>> mapEntrySetIterator = mapEntrySet.iterator();
                    return new Iterator<>() {
                        K currentKey;

                        @Override
                        public boolean hasNext() {
                            boolean rt = mapEntrySetIterator.hasNext();
                            track(lengthDeps);
                            return rt;
                        }

                        @Override
                        public Entry<K, V> next() {
                            Entry<K, V> rt = mapEntrySetIterator.next();
                            K key = rt.getKey();
                            currentKey = key;
                            return new Entry<>() {
                                @Override
                                public K getKey() {
                                    return key;
                                }

                                @Override
                                public V getValue() {
                                    V value = rt.getValue();
                                    trackKey(key);
                                    return value;
                                }

                                @Override
                                public V setValue(V value) {
                                    V oldValue = rt.setValue(value);
                                    if (differ(oldValue, value)) {
                                        triggerKey(key);
                                    }
                                    return oldValue;
                                }
                            };
                        }

                        @Override
                        public void remove() {
                            Iterator.super.remove();
                            triggerKey(currentKey);
                            trigger(lengthDeps);
                        }
                    };
                }

                @Override
                public int size() {
                    int length = mapEntrySet.size();
                    track(lengthDeps);
                    return length;
                }
            };
        }

        @Override
        public boolean containsKey(Object key) {
            boolean rt = map.containsKey(key);
            track(lengthDeps);
            return rt;
        }

        @Override
        public V get(Object key) {
            @SuppressWarnings("unchecked")
            K k = (K)key;
            V value = map.get(k);
            trackKey(k);
            return value;
        }

        @Override
        public V put(K key, V value) {
            boolean lengthChanged = !map.containsKey(key);
            V oldValue = map.put(key, value);
            if (differ(oldValue, value)) {
                triggerKey(key);
            }
            if (lengthChanged) {
                trigger(lengthDeps);
            }
            return oldValue;
        }

        @Override
        public V remove(Object key) {
            @SuppressWarnings("unchecked")
            K k = (K)key;
            V value = map.remove(k);
            triggerKey(k);
            trigger(lengthDeps);
            return value;
        }
    }

    private final class Effect<T> implements Supplier<T> {
        final Supplier<T> func;

        final Consumer<Supplier<T>> scheduler;

        final Map<Set<Effect<?>>, Object> deps = new IdentityHashMap<>();

        Effect(Supplier<T> func, Consumer<Supplier<T>> scheduler) {
            this.func = func;
            this.scheduler = scheduler;
        }

        void update() {
            if (scheduler != null) {
                scheduler.accept(this);
            } else {
                get();
            }
        }

        @Override
        public T get() {
            deps.forEach((deps, v) -> deps.remove(this));
            deps.clear();
            Effect<?> lastRunning = running;
            running = this;
            try {
                return func.get();
            } finally {
                running = lastRunning;
            }
        }
    }

    private Effect<?> running;

    private void track(Set<Effect<?>> deps) {
        if (running != null) {
            deps.add(running);
            running.deps.put(deps, null);
        }
    }

    private void trigger(Set<Effect<?>> deps) {
        if (running != null) {
            throw new IllegalStateException("Reactive value modified in an effect function");
        }
        Effect<?>[] effects = deps.toArray(new Effect[0]);
        for (Effect<?> effect : effects) {
            effect.update();
        }
    }

    public <T> IRef<T> ref(T item) {
        return new ReactiveRef<>(item);
    }

    public <T> List<T> reactiveList(List<T> list) {
        return new ReactiveList<>(list);
    }

    public <K, V> ReactiveMap<K, V> reactiveMap(Map<K, V> map) {
        return new ReactiveMap<>(map);
    }

    public <T> T reactiveObject(Class<T> clazz) {
        if (clazz == null || !clazz.isInterface()) {
            throw new IllegalArgumentException("Argument clazz should be an interface");
        }
        Map<String, Class<?>> types = new HashMap<>();
        Map<String, IRef<Object>> refs = new HashMap<>();
        Map<Method, Function<Object, Object>> methodProxies = new HashMap<>();
        for (Method method : clazz.getMethods()) {
            if (method.isAnnotationPresent(Getter.class)) {
                if (method.isAnnotationPresent(Setter.class)) {
                    throw new IllegalArgumentException("Method '" + method.getName() + "' cannot be both a getter and a setter");
                }
                if (method.getParameterCount() > 0) {
                    throw new IllegalArgumentException("Getter '" + method.getName() + "' should not have parameters");
                }
                Class<?> returnType = method.getReturnType();
                if (returnType.equals(Void.class)) {
                    throw new IllegalArgumentException("Getter '" + method.getName() + "' should return a value");
                }
                String name = method.getAnnotation(Getter.class).value();
                Class<?> type = types.computeIfAbsent(name, k -> returnType);
                if (type != returnType) {
                    throw new IllegalArgumentException("Different types of '" + name + "' found");
                }
                IRef<?> ref = refs.computeIfAbsent(name, k -> ref(null));
                methodProxies.put(method, o -> ref.get());
            } else if (method.isAnnotationPresent(Setter.class)) {
                if (method.getParameterCount() != 1) {
                    throw new IllegalArgumentException("Setter '" + method.getName() + "' should have exactly 1 parameter");
                }
                if (method.getReturnType().equals(Void.class)) {
                    System.out.println(method.getReturnType());
                    throw new IllegalArgumentException("Setter '" + method.getName() + "' should return void");
                }
                String name = method.getAnnotation(Setter.class).value();
                Class<?> paramType = method.getParameters()[0].getType();
                Class<?> type = types.computeIfAbsent(name, k -> paramType);
                if (type != paramType) {
                    throw new IllegalArgumentException("Different types of '" + name + "' found");
                }
                IRef<Object> ref = refs.computeIfAbsent(name, k -> ref(null));
                methodProxies.put(method, o -> {
                    ref.set(o);
                    return null;
                });
            } else {
                throw new IllegalArgumentException("No reactive annotations found on method '" + method.getName() + "'");
            }
        }
        return clazz.cast(
                Proxy.newProxyInstance(
                        clazz.getClassLoader(),
                        new Class[]{clazz, ReactiveObject.class},
                        (proxy, method, args) -> methodProxies.get(method).apply(args == null ? null : args[0])
                )
        );
    }

    public <T> Supplier<T> effect(Supplier<T> func, Consumer<Supplier<T>> scheduler, boolean lazy) {
        Effect<T> effect = new Effect<>(func, scheduler);
        if (!lazy) {
            effect.get();
        }
        return effect;
    }

    public Runnable effect(Runnable func, Consumer<Runnable> scheduler, boolean lazy) {
        return effect(() -> {
            func.run();
            return null;
        }, scheduler == null ? null : f -> scheduler.accept(f::get), lazy)::get;
    }

    public <T> Supplier<T> computed(Supplier<T> func) {
        final class Result<U> {
            U value;
            boolean dirty = true;
        }
        Result<T> result = new Result<>();
        Set<Effect<?>> deps = new HashSet<>();
        Supplier<T> effect = effect(func, f -> {
            if (!result.dirty) {
                result.dirty = true;
                trigger(deps);
            }
        }, true);
        return () -> {
            if (result.dirty) {
                result.value = effect.get();
                result.dirty = false;
            }
            track(deps);
            return result.value;
        };
    }

    public <T> void watch(Supplier<T> func, BiConsumer<T, T> callback, boolean immediate) {
        final class Result<U> {
            U oldValue;
            U newValue;
        }
        Result<T> result = new Result<>();
        Consumer<Supplier<T>> scheduler = f -> {
            result.newValue = f.get();
            callback.accept(result.newValue, result.oldValue);
            result.oldValue = result.newValue;
        };
        Supplier<T> effect = effect(func, scheduler, false);
        if (immediate) {
            scheduler.accept(effect);
        } else {
            result.oldValue = effect.get();
        }
    }

    public <T> void watch(IRef<T> ref, BiConsumer<T, T> callback, boolean immediate) {
        watch(ref::get, callback, immediate);
    }
}
